Implement adopt-not-replace sync with remote .md file tracking#59
Conversation
Treat the WebDAV/NAS Markdown files as the canonical note representation and merge them into the local DB instead of wiping local rows on every pull. Each adopted note now carries the server-side identity (`remote_id` plus `remote_path` derived from synthetic `md.<base64>` ids) so repeated syncs upsert in place rather than minting duplicates. Push side respects the adopted identity and never POSTs a note that already has a remote_id. Conflict policy keeps both rows when titles collide on different ids and prefers the newer `updated_at` on identity matches. Display titles are stripped of `.md` and `__<id-prefix>` slug artifacts at the API ingestion boundary so the corrupted `__Md.Q2Hhd` form never reaches the UI. https://claude.ai/code/session_01T83ptxrcqCCk9Yx7WerGUd
There was a problem hiding this comment.
💡 Codex Review
NexaNote/app/lib/services/sync_service.dart
Lines 83 to 85 in 668dcb6
After createNotebook succeeds, the local notebook is left unchanged (sync_status stays non-synced and no server ID mapping is stored), but pullRemote now uses adopt-not-replace semantics. In a normal sync loop, that means the same local notebook is posted again on every subsequent sync(), and each pull adds another remote copy instead of reconciling the original. This regression is introduced by the new merge strategy because the old replace-all pull used to wipe the unsynced local row.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (note.remoteId != null && note.remoteId!.isNotEmpty) continue; | ||
| await _api.createNote( | ||
| title: note.title, | ||
| title: cleanRemoteTitle(note.title), | ||
| noteType: note.noteType, | ||
| notebookId: note.notebookId, | ||
| ); |
There was a problem hiding this comment.
Persist pushed-note linkage to avoid re-posting duplicates
pushLocal posts unsynced notes but ignores the created remote note metadata, so the local row keeps no remoteId and remains non-synced. With adopt-not-replace pull, the returned server note is treated as a different record (often causing a conflict-marked local copy plus adopted remote copy), and the original local note is posted again on the next sync. This creates an unbounded duplicate cycle for newly created local notes.
Useful? React with 👍 / 👎.
Summary
Overhaul the sync engine to adopt remote notes into the local database instead of replacing it wholesale. This enables idempotent pulls, preserves local-only work, and tracks remote
.mdfiles by their canonical server id to avoid duplicates on re-sync.Key Changes
Adopt-not-replace pull strategy: Remote notebooks and notes are now upserted in place rather than wiping the local DB. Running
pullRemote()multiple times yields the same row set (idempotent).Remote id tracking: Added
remote_idandremote_pathcolumns to thenotestable (schema v1 → v2) so adopted notes can be matched on subsequent pulls without creating duplicates. Plain Markdown files with syntheticmd.<base64>ids are decoded to recover their canonical path.Push guard for adopted notes:
pushLocal()now skips any note that already carries aremoteId, preventing duplicate creation via the non-idempotentPOST /notesendpoint.Conflict detection: When a remote note arrives with the same cleaned title as an unrelated local row (different id), both are kept and the local row is marked
sync_status='conflict'.Timestamp-based merge: When a remote note matches a local one by id, the side with the newer
updatedAtwins—except localmodifiedrows beat older remotes to preserve in-flight edits.Title cleanup: Extracted
cleanRemoteTitle()into a dedicated module to strip.mdextensions and WebDAV slug suffixes (__<id-prefix>) that leak into note titles from the filesystem.Selective deletion: Synced local notes that disappear from the remote are removed (server-side delete propagates), but local-only and modified notes survive a pull.
Database migration: Added
Schema.onUpgrade()to safely add the new columns when upgrading from v1 to v2.Implementation Details
SyncResultnow includesnotesAdoptedcount to distinguish newly-adopted remote notes from those already in the local DB._PullCountsand_PushCountsseparate the return types for clarity.LocalNoteServiceexposes new upsert and hard-delete methods (upsertNote,upsertNotebook,hardDeleteNote,hardDeleteNotebook) used by the sync engine.NoteRepository.getNoteByRemoteId()enables lookup of locally-adopted notes by their remote id.https://claude.ai/code/session_01T83ptxrcqCCk9Yx7WerGUd